﻿using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Administration.Claims;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.WebControls;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
using System.Linq;
using System.Text;
using System.Threading;

/*
 * DO NOT directly edit LDAPCP class. It is designed to be inherited to customize it as desired.
 * Please download "LDAPCP for Developers.zip" on http://ldapcp.codeplex.com to find examples and guidance.
 * */

namespace ldapcp
{
    /// <summary>
    /// Provides search and resolution against Active Directory or any LDAP server.
    /// Visit http://ldapcp.codeplex.com/ for documentation and updates.
    /// Please report any bug to http://ldapcp.codeplex.com/.
    /// Author: Yvan Duhamel
    /// Copyright (c) 2013, Yvan Duhamel. All rights reserved.
    /// </summary>
    public class LDAPCP : SPClaimProvider
    {
        public const string _ProviderInternalName = "LDAPCP";
        public virtual string ProviderInternalName { get { return "LDAPCP"; } }

        /// <summary>
        /// Contains configuration currently in use by claims provider
        /// </summary>
        public ILDAPCPConfiguration CurrentConfiguration;

        private object Sync_Init = new object();
        private ReaderWriterLockSlim Lock_Config = new ReaderWriterLockSlim();
        private long LdapcpConfigVersion = 0;

        /// <summary>
        /// Contains the attribute mapped to the identity claim in the SPTrustedLoginProvider
        /// </summary>
        protected AttributeHelper IdentityAttribute;

        /// <summary>
        /// Contains attributes that are not used in the filter (both ClaimTypeProp AND CreateAsIdentityClaim are not set), but have EntityDataKey set
        /// </summary>
        protected List<AttributeHelper> UserMetadataAttributes;

        /// <summary>
        /// SPTrust associated with the claims provider
        /// </summary>
        protected SPTrustedLoginProvider SPTrust;

        /// <summary>
        /// List of attributes actually defined in the trust
        /// + list of LDAP attributes that are always queried, even if not defined in the trust (typically the displayName)
        /// </summary>
        private List<AttributeHelper> ProcessedAttributes;

        protected virtual string LDAPObjectClassName { get { return "objectclass"; } }
        protected virtual string LDAPFilter { get { return "(&(" + LDAPObjectClassName + "={2})({0}={1}){3}) "; } }
        protected virtual string PickerEntityDisplayText { get { return "({0}) {1}"; } }
        protected virtual string PickerEntityOnMouseOver { get { return "{0}={1}"; } }
        protected virtual string EnabledUsersOnlyLDAPFilter { get { return "(&(!(userAccountControl:1.2.840.113556.1.4.803:=2))"; } }
        protected virtual string FilterSecurityGroupsOnlyLDAPFilter { get { return "(groupType:1.2.840.113556.1.4.803:=2147483648)"; } }

        protected string IssuerName
        {
            get
            {
                // The advantage of using the SPTrustedLoginProvider name for the issuer name is that it makes possible and easy to replace current claims provider with another one.
                // The other claims provider would simply have to use SPTrustedLoginProvider name too
                return SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, SPTrust.Name);
            }
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="displayName"></param>
        public LDAPCP(string displayName)
            : base(displayName)
        {
            LDAPCP.LogToULS(String.Format("[{0}] Constructor called", ProviderInternalName), TraceSeverity.VerboseEx, EventSeverity.Information, LdapcpLoggingService.Categories.LDAPCP);
        }

        /// <summary>
        /// Initializes claim provider. This method is reserved for internal use and is not intended to be called from external code or changed
        /// </summary>
        protected bool Initialize(Uri context, string[] entityTypes)
        {
            // Ensures thread safety to initialize class variables
            lock (Sync_Init)
            {
                // 1ST PART: GET CONFIGURATION OBJECT
                LDAPCPConfig globalConfiguration = null;
                bool refreshConfig = false;
                bool success = true;
                bool initializeFromPersistedObject = true;
                try
                {
                    if (SPTrust == null)
                    {
                        if (!SetSPTrustInCurrentContext(context)) return false;
                    }

                    // Should not try to get PersistedObject if not OOB LDAPCP since with current design it works correctly only for OOB LDAPCP
                    if (String.Equals(ProviderInternalName, LDAPCP._ProviderInternalName, StringComparison.InvariantCultureIgnoreCase))
                    {
                        globalConfiguration = LDAPCPConfig.GetFromConfigDB();
                        if (globalConfiguration == null)
                        {
                            LDAPCP.LogToULS(String.Format("[{0}] LdapcpConfig PersistedObject not found. Visit LDAPCP admin pages in central administration to create it.", ProviderInternalName),
                                TraceSeverity.Verbose, EventSeverity.Information, LdapcpLoggingService.Categories.LDAPCP);
                            // Create a fake persisted object just to get the default settings, it will not be saved in config database
                            globalConfiguration = LDAPCPConfig.GetDefaultSettings(new LDAPCPConfig());
                        }
                        else if (globalConfiguration.AttributesListProp == null || globalConfiguration.AttributesListProp.Count == 0)
                        {
                            LDAPCP.LogToULS(String.Format("[{0}] LdapcpConfig PersistedObject was found but there are no Attribute set. Visit AzureCP admin pages in central administration to create it.", ProviderInternalName),
                                TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.LDAPCP);
                            // Cannot continue 
                            success = false;
                        }
                        else if (globalConfiguration.LDAPConnectionsProp == null || globalConfiguration.LDAPConnectionsProp.Count == 0)
                        {
                            LDAPCP.LogToULS(String.Format("[{0}] LdapcpConfig PersistedObject was found but there are no LDAP connection set. Visit AzureCP admin pages in central administration to create it.", ProviderInternalName),
                                TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.LDAPCP);
                            // Cannot continue 
                            success = false;
                        }
                        else
                        {
                            // Persisted object is found and seems valid
                            LDAPCP.LogToULS(String.Format("[{0}] LdapcpConfig PersistedObject found, version: {1}, previous version: {2}", ProviderInternalName, globalConfiguration.Version.ToString(), this.LdapcpConfigVersion.ToString()),
                                TraceSeverity.Verbose, EventSeverity.Information, LdapcpLoggingService.Categories.LDAPCP);
                            if (this.LdapcpConfigVersion != globalConfiguration.Version)
                            {
                                refreshConfig = true;
                                this.LdapcpConfigVersion = globalConfiguration.Version;
                                LDAPCP.LogToULS(String.Format("[{0}] LdapcpConfig PersistedObject changed, refreshing configuration", ProviderInternalName),
                                    TraceSeverity.Medium, EventSeverity.Information, LdapcpLoggingService.Categories.LDAPCP);
                            }
                        }
                    }
                    else
                    {
                        // LDAPCP class inherited, refresh config only needed if ProcessedAttributes null
                        initializeFromPersistedObject = false;
                        if (this.ProcessedAttributes == null) refreshConfig = true;

                        // Failed to get it from config database, which is expected if LDAPCP is inherited.
                        // Create a fake persisted object just to get the default settings, it will not be saved in config database
                        globalConfiguration = LDAPCPConfig.GetDefaultSettings(new LDAPCPConfig());
                        LDAPCP.LogToULS(
                            String.Format("[{0}] LDAPCP class inherited", ProviderInternalName),
                            TraceSeverity.Verbose, EventSeverity.Information, LdapcpLoggingService.Categories.LDAPCP);
                    }
                }
                catch (Exception ex)
                {
                    success = false;
                    LogToULS(String.Format("[{0}] Unexpected error in Initialize: {1}. Callstack: {2}", ProviderInternalName, ex.Message, ex.StackTrace), TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.LDAPCP);
                }
                finally
                {
                    // There are multiple reasons why ProcessedAttributes can be null at this point:
                    // SPTrust is null: code returns to caller immediately after finally statement
                    // 1st initialization
                    // Initialized before but it failed. If so, config refreshed should try again and Initialize would return false it if fails again (callers rely on that return value)
                    if (this.ProcessedAttributes == null) refreshConfig = true;

                    // If LdapcpConfig, something went wrong and Initialization cannot continue
                    if (globalConfiguration == null) success = false;
                }

                if (!success) return success;
                if (!refreshConfig) return success;

                // 2ND PART: APPLY CONFIGURATION
                // Configuration needs to be refreshed, lock current thread in write mode
                Lock_Config.EnterWriteLock();
                try
                {
                    LDAPCP.LogToULS(String.Format("[{0}] Refreshing configuration", ProviderInternalName),
                        TraceSeverity.Verbose, EventSeverity.Information, LdapcpLoggingService.Categories.LDAPCP);

                    // Create local persisted object that will never be saved in config DB, it's just a local copy
                    this.CurrentConfiguration = new LDAPCPConfig();
                    if (initializeFromPersistedObject)
                    {
                        // All settings come from persisted object
                        this.CurrentConfiguration.AlwaysResolveUserInputProp = globalConfiguration.AlwaysResolveUserInputProp;
                        this.CurrentConfiguration.AddWildcardInFrontOfQueryProp = globalConfiguration.AddWildcardInFrontOfQueryProp;
                        this.CurrentConfiguration.PickerEntityGroupNameProp = globalConfiguration.PickerEntityGroupNameProp;
                        this.CurrentConfiguration.DisplayLdapMatchForIdentityClaimTypeProp = globalConfiguration.DisplayLdapMatchForIdentityClaimTypeProp;
                        this.CurrentConfiguration.FilterEnabledUsersOnlyProp = globalConfiguration.FilterEnabledUsersOnlyProp;
                        this.CurrentConfiguration.FilterSecurityGroupsOnlyProp = globalConfiguration.FilterSecurityGroupsOnlyProp;
                        this.CurrentConfiguration.FilterExactMatchOnlyProp = globalConfiguration.FilterExactMatchOnlyProp;

                        //this._LDAPCPIssuerType = LdapcpConfig.LDAPCPIssuerType;
                        //// It can be set to Unknown (default value) if the persisted object in db doesn't contain an entry for it (not updated since last update)
                        //// Since Unknown is invalid and generates an exception in SPOriginalIssuers.Format(), enforce it to its default (and mostly expected) value
                        //if (this._LDAPCPIssuerType == SPOriginalIssuerType.Unknown) this._LDAPCPIssuerType = SPOriginalIssuerType.TrustedProvider;

                        // Retrieve AttributesListProp
                        this.CurrentConfiguration.AttributesListProp = new List<AttributeHelper>();
                        foreach (AttributeHelper currentObject in globalConfiguration.AttributesListProp)
                        {
                            // Create a copy from persisted object
                            this.CurrentConfiguration.AttributesListProp.Add(currentObject.CopyPersistedProperties());
                        }

                        // Retrieve LDAPConnections. DirectoryEntry classes are created later from LDAPConnectionsProp
                        this.CurrentConfiguration.LDAPConnectionsProp = new List<LDAPConnection>();
                        foreach (LDAPConnection currentCoco in globalConfiguration.LDAPConnectionsProp)
                        {
                            // Create a copy from persisted object
                            this.CurrentConfiguration.LDAPConnectionsProp.Add(currentCoco.CopyPersistedProperties());
                        }
                    }
                    else
                    {
                        this.CurrentConfiguration = LDAPCPConfig.GetDefaultSettings(this.CurrentConfiguration as LDAPCPConfig);
                        SetCustomConfiguration(context, entityTypes);
                        if (this.CurrentConfiguration.AttributesListProp == null)
                        {
                            // this.CurrentConfiguration.AttributesListProp was set to null in SetCustomConfiguration, which is bad
                            LogToULS(String.Format("[{0}] AttributesListProp was not set to null in SetCustomConfiguration, don't set it or set it with actual entries.", ProviderInternalName), TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.LDAPCP);
                            return false;
                        }

                        // Use default LDAP connection that may be overriden in SetLDAPConnections methods
                        this.CurrentConfiguration.LDAPConnectionsProp = LDAPCPConfig.GetDefaultLDAPConnection();
                    }
                    success = ProcessAttributesList(this.CurrentConfiguration.AttributesListProp);
                }
                catch (Exception ex)
                {
                    success = false;
                    LogToULS(String.Format("[{0}] Unexpected error in Initialize to refresh configuration: {1}. Callstack: {2}", ProviderInternalName, ex.Message, ex.StackTrace), TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.LDAPCP);
                }
                finally
                {
                    Lock_Config.ExitWriteLock();
                }
                return success;
            }
        }

        /// <summary>
        /// Initializes claim provider. This method is reserved for internal use and is not intended to be called from external code or changed
        /// </summary>
        private bool ProcessAttributesList(List<AttributeHelper> attributeHelperCollection)
        {
            bool success = true;
            try
            {
                bool identityClaimTypeFound = false;
                // Get attributes defined in trust based on their claim type (unique way to map them)
                List<AttributeHelper> attributesDefinedInTrust = new List<AttributeHelper>();
                // There is a bug in the SharePoint API: SPTrustedLoginProvider.ClaimTypes should retrieve SPTrustedClaimTypeInformation.MappedClaimType, but it returns SPTrustedClaimTypeInformation.InputClaimType instead, so we cannot rely on it
                //foreach (var attr in attributeHelperCollection.Where(x => SPTrust.ClaimTypes.Contains(x.ClaimTypeProp)))
                //{
                //    attributesDefinedInTrust.Add(attr);
                //}
                foreach (SPTrustedClaimTypeInformation claimTypeInformation in SPTrust.ClaimTypeInformation)
                {
                    //attributesDefinedInTrust.Add(attributeHelperCollection.First(x => String.Equals(x.ClaimTypeProp, ClaimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase) && !x.CreateAsIdentityClaim));
                    List<AttributeHelper> attObjectColl = attributeHelperCollection.FindAll(x =>
                        String.Equals(x.ClaimType, claimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase) &&
                        !x.CreateAsIdentityClaim &&
                        !String.IsNullOrEmpty(x.LDAPAttribute) &&
                        !String.IsNullOrEmpty(x.LDAPObjectClassProp));
                    AttributeHelper att;
                    if (attObjectColl.Count == 1)
                    {
                        att = attObjectColl.First();
                        attributesDefinedInTrust.Add(att);

                        if (String.Equals(SPTrust.IdentityClaimTypeInformation.MappedClaimType, att.ClaimType, StringComparison.InvariantCultureIgnoreCase))
                        {
                            // Identity claim type found, set IdentityAttribute property
                            identityClaimTypeFound = true;
                            IdentityAttribute = att;
                        }
                    }
                }

                // Make sure that the identity claim is in this collection. Should always check property SPTrustedClaimTypeInformation.MappedClaimType: http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.administration.claims.sptrustedclaimtypeinformation.mappedclaimtype.aspx
                if (!identityClaimTypeFound)
                {
                    LogToULS(String.Format("[{0}] Impossible to continue because identity claim type \"{1}\" set in the SPTrustedIdentityTokenIssuer \"{2}\" is missing in the LDAPCP claim types list. Add it from central admin > security > claims mapping page", ProviderInternalName, SPTrust.IdentityClaimTypeInformation.MappedClaimType, SPTrust.Name), TraceSeverity.Unexpected, EventSeverity.ErrorCritical, LdapcpLoggingService.Categories.LDAPCP);
                    return false;
                }

                // Check if there are attributes that should be always queried (CreateAsIdentityClaim) to add in the list
                List<AttributeHelper> additionalAttributes = new List<AttributeHelper>();
                foreach (var attr in attributeHelperCollection.Where(x => x.CreateAsIdentityClaim && !attributesDefinedInTrust.Contains(x, new LDAPPropertiesComparer())))
                {
                    if (String.Equals(SPTrust.IdentityClaimTypeInformation.MappedClaimType, attr.ClaimType))
                    {
                        // Not a big deal since it's set with identity claim type, so no inconsistent behavior to expect, just record an information
                        LogToULS(String.Format("[{0}] Object with LDAP attribute/class {1}/{2} is set with CreateAsIdentityClaim to true and ClaimTypeProp {3}. Remove ClaimTypeProp property as it is useless.", ProviderInternalName, attr.LDAPAttribute, attr.LDAPObjectClassProp, attr.ClaimType), TraceSeverity.Monitorable, EventSeverity.Information, LdapcpLoggingService.Categories.LDAPCP);
                    }
                    else if (attributesDefinedInTrust.Count(x => String.Equals(x.ClaimType, attr.ClaimType)) > 0)
                    {
                        // Same claim type already exists with CreateAsIdentityClaim == false. 
                        // Current object is a bad one and shouldn't be added. Don't add it but continue to build objects list
                        LogToULS(String.Format("[{0}] Claim type {1} is defined twice with CreateAsIdentityClaim set to true and false, which is invalid. Remove entry with CreateAsIdentityClaim set to true.", ProviderInternalName, attr.ClaimType), TraceSeverity.Monitorable, EventSeverity.Information, LdapcpLoggingService.Categories.LDAPCP);
                        continue;
                    }

                    attr.ClaimType = SPTrust.IdentityClaimTypeInformation.MappedClaimType;
                    attr.ClaimEntityType = SPClaimEntityTypes.User;
                    attr.LDAPAttributeToDisplayProp = IdentityAttribute.LDAPAttributeToDisplayProp; // Must be set otherwise display text of permissions will be inconsistent
                    additionalAttributes.Add(attr);
                }

                this.ProcessedAttributes = new List<AttributeHelper>(attributesDefinedInTrust.Count + additionalAttributes.Count);
                this.ProcessedAttributes.AddRange(attributesDefinedInTrust);
                this.ProcessedAttributes.AddRange(additionalAttributes);

                // Parse each attribute to configure its settings from the corresponding claim types defined in the SPTrustedLoginProvider
                foreach (var attr in this.ProcessedAttributes)
                {
                    var trustedClaim = SPTrust.GetClaimTypeInformationFromMappedClaimType(attr.ClaimType);
                    // It should never be null
                    if (trustedClaim == null) continue;
                    attr.ClaimTypeMappingName = trustedClaim.DisplayName;
                }

                // Any metadata for a user with at least an LDAP attribute and a LDAP class is valid
                this.UserMetadataAttributes = attributeHelperCollection.FindAll(x =>
                    !String.IsNullOrEmpty(x.EntityDataKey) &&
                    !String.IsNullOrEmpty(x.LDAPAttribute) &&
                    !String.IsNullOrEmpty(x.LDAPObjectClassProp) &&
                    x.ClaimEntityType == SPClaimEntityTypes.User);
            }
            catch (Exception ex)
            {
                LogToULS(String.Format("[{0}] Unexpected error while building attributes list: {1}, Callstack: {2}.", ProviderInternalName, ex.Message, ex.StackTrace), TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.LDAPCP);
                success = false;
            }
            return success;
        }

        /// <summary>
        /// Override this method to customize configuration of LDAPCP
        /// </summary>
        /// <param name="context">The context, as a URI</param>
        /// <param name="entityTypes">The EntityType entity types set to scope the search to</param>
        protected virtual void SetCustomConfiguration(Uri context, string[] entityTypes)
        {
        }

        protected virtual bool SetSPTrustInCurrentContext(Uri context)
        {
            if (context == null)
            {
                SPTrust = GetSPTrustAssociatedWithCP(ProviderInternalName);
                if (SPTrust == null) return false;
                return true;
            }

            var webApp = SPWebApplication.Lookup(context);
            if (webApp == null) return false;

            if (webApp.IsAdministrationWebApplication)
            {
                // If current site is central administration (can happen if admin sets a user in a SA, should just check if claims provider is associated with a trust
                SPTrust = LDAPCP.GetSPTrustAssociatedWithCP(ProviderInternalName);
                if (SPTrust == null) return false;
                return true;
            }

            // Not central admin web app, show claims provider only if current web app needs it
            // Consider following scenario: default zone is NTLM, intranet zone is claims
            // In intranet zone, when creating permission, claim provider will get request 2 times for default and intranet zone
            // So it cannot exclude zones where it is not used
            // otherwise permissions cannot be created and SharePoint will throw error "The user does not exist or is not unique"
            foreach (var zone in Enum.GetValues(typeof(SPUrlZone)))
            {
                SPIisSettings iisSettings = webApp.GetIisSettingsWithFallback((SPUrlZone)zone);

                if (!iisSettings.UseTrustedClaimsAuthenticationProvider)
                    continue;

                // Get the list of authentication providers associated with the zone
                foreach (SPAuthenticationProvider prov in iisSettings.ClaimsAuthenticationProviders)
                {
                    if (prov.GetType() == typeof(Microsoft.SharePoint.Administration.SPTrustedAuthenticationProvider))
                    {
                        // Check if the current SPTrustedAuthenticationProvider is associated with the claim provider
                        if (String.Equals(prov.ClaimProviderName, ProviderInternalName, StringComparison.OrdinalIgnoreCase))
                        {
                            SPTrust = LDAPCP.GetSPTrustAssociatedWithCP(ProviderInternalName);
                            return SPTrust == null ? false : true;
                        }
                    }
                }
            }
            return false;
        }

        /// <summary>
        /// Get the first TrustedLoginProvider associated with current claim provider
        /// LIMITATION: The same claims provider (uniquely identified by its name) cannot be associated to multiple TrustedLoginProvider because at runtime there is no way to determine what TrustedLoginProvider is currently calling
        /// </summary>
        /// <param name="ProviderInternalName"></param>
        /// <returns></returns>
        public static SPTrustedLoginProvider GetSPTrustAssociatedWithCP(string ProviderInternalName)
        {
            var lp = SPSecurityTokenServiceManager.Local.TrustedLoginProviders.Where(x => String.Equals(x.ClaimProviderName, ProviderInternalName, StringComparison.OrdinalIgnoreCase));

            if (lp != null && lp.Count() == 1)
                return lp.First();

            if (lp != null && lp.Count() > 1)
                LogToULS(String.Format("[{0}] Claims provider {0} is associated to multiple SPTrustedIdentityTokenIssuer, which is not supported because at runtime there is no way to determine what TrustedLoginProvider is currently calling", ProviderInternalName), TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.LDAPCP);

            LogToULS(String.Format("[{0}] Claims provider {0} is not associated with any SPTrustedIdentityTokenIssuer so it cannot create permissions.\r\nVisit http://ldapcp.codeplex.com for installation procedure or set property ClaimProviderName with PowerShell cmdlet Get-SPTrustedIdentityTokenIssuer to create association.", ProviderInternalName), TraceSeverity.High, EventSeverity.Warning, LdapcpLoggingService.Categories.LDAPCP);
            return null;
        }

        protected void BuildFilterAndProcessResults(string input, List<AttributeHelper> attributesToQuery, bool exactSearch, Uri context, string[] entityTypes, ref ConsolidatedResultCollection ConsolidatedResults)
        {
            // Build LDAP filter as documented in http://technet.microsoft.com/fr-fr/library/aa996205(v=EXCHG.65).aspx
            StringBuilder filter = new StringBuilder();
            if (this.CurrentConfiguration.FilterEnabledUsersOnlyProp) filter.Append(EnabledUsersOnlyLDAPFilter);
            filter.Append("(| ");

            string searchPattern;
            if (exactSearch)
                searchPattern = input;
            else
                searchPattern = this.CurrentConfiguration.AddWildcardInFrontOfQueryProp ? "*" + input + "*" : input + "*";

            foreach (var attribute in attributesToQuery)
            {
                filter.Append(AddAttributeToFilter(attribute, searchPattern));
            }
            if (this.CurrentConfiguration.FilterEnabledUsersOnlyProp) filter.Append(")");
            filter.Append(")");

            bool resultsfound = false;
            //SearchResult[] LDAPSearchResults = null;
            List<LDAPSearchResultWrapper> LDAPSearchResultWrappers = new List<LDAPSearchResultWrapper>();
            using (new SPMonitoredScope(String.Format("[{0}] Total time spent in all LDAP server(s)", ProviderInternalName), 1000))
            {
                resultsfound = SearchObjectsInLDAP(filter.ToString(), context, entityTypes, ref LDAPSearchResultWrappers);
            }

            if (!resultsfound)
            {
                LogToULS(String.Format("[{0}] This LDAP query did not return any result: \"{1}\"", ProviderInternalName, filter), TraceSeverity.Medium, EventSeverity.Information, LdapcpLoggingService.Categories.LDAP_Lookup);
                return;
            }

            LogToULS(String.Format("[{0}] Got {1} result(s) from all LDAP server(s) with query \"{2}\"", ProviderInternalName, LDAPSearchResultWrappers.Count, filter), TraceSeverity.Medium, EventSeverity.Information, LdapcpLoggingService.Categories.LDAP_Lookup);
            ResultPropertyCollection resultPropertyCollection;

            // If exactSearch is true, we don't care about attributes with CreateAsIdentityClaim = true
            List<AttributeHelper> attributes;
            if (exactSearch) attributes = attributesToQuery.FindAll(x => !x.CreateAsIdentityClaim);
            else attributes = attributesToQuery;

            foreach (LDAPSearchResultWrapper LDAPresult in LDAPSearchResultWrappers)
            {
                resultPropertyCollection = LDAPresult.SearchResult.Properties;
                // objectclass attribute should never be missing because it is explicitely requested in LDAP query
                if (!resultPropertyCollection.Contains(LDAPObjectClassName))
                    continue;

                foreach (var attr in attributes)
                {
                    // Check if current attribute object class matches the current LDAP result
                    if (!resultPropertyCollection[LDAPObjectClassName].Contains(attr.LDAPObjectClassProp))
                        continue;

                    // Check if current LDAP result contains LDAP attribute of current attribute
                    if (!resultPropertyCollection.Contains(attr.LDAPAttribute))
                        continue;

                    string value = resultPropertyCollection[attr.LDAPAttribute][0].ToString();
                    // Check if current attribute matches the input
                    if (exactSearch)
                    {
                        if (!String.Equals(value, input, StringComparison.InvariantCultureIgnoreCase)) continue;
                    }
                    else
                    {
                        if (!value.StartsWith(input, StringComparison.InvariantCultureIgnoreCase)) continue;
                    }

                    // Add to collection of objectclass/ldap attribute in list of results if it doesn't already exist
                    AttributeHelper objCompare;
                    if (attr.CreateAsIdentityClaim)
                    {
                        if (!resultPropertyCollection.Contains(IdentityAttribute.LDAPAttribute)) continue;
                        // If exactSearch is true, then IdentityAttribute.LDAPAttribute value should be also equals to input, otherwise igno
                        objCompare = IdentityAttribute;
                    }
                    else
                    {
                        objCompare = attr;
                    }
                    if (ConsolidatedResults.Contains(LDAPresult, objCompare, this.CurrentConfiguration.CompareResultsWithLDAPNetBiosName))
                        continue;

                    ConsolidatedResults.Add(
                        new ConsolidatedResult
                        {
                            Attribute = attr,
                            LDAPResults = resultPropertyCollection,
                            Value = value,
                            NetBiosName = LDAPresult.LDAPNetBiosName,
                            //DEBUG = String.Format("LDAPAttribute: {0}, LDAPAttributeValue: {1}, AlwaysResolveAgainstIdentityClaim: {2}", attr.LDAPAttribute, resultPropertyCollection[attr.LDAPAttribute][0].ToString(), attr.AlwaysResolveAgainstIdentityClaim.ToString())
                        });
                }
            }

            LogToULS(String.Format("[{0}] {1} permission(s) to create after filtering", ProviderInternalName, ConsolidatedResults.Count), TraceSeverity.Medium, EventSeverity.Information, LdapcpLoggingService.Categories.LDAP_Lookup);
            foreach (var result in ConsolidatedResults)
            {
                PickerEntity pe = CreatePickerEntityHelper(result);
                // Add it to the return list of picker entries.
                result.PickerEntity = pe;
            }

            if (ConsolidatedResults.Count == 0) ConsolidatedResults = null;
        }

        protected override void FillClaimTypes(List<string> claimTypes)
        {
            if (claimTypes == null)
                throw new ArgumentNullException("claimTypes");

            LDAPCP.LogToULS(String.Format("[{0}] FillClaimValueTypes called, ProcessedAttributes null: {1}", ProviderInternalName, ProcessedAttributes == null ? true : false),
                TraceSeverity.VerboseEx, EventSeverity.Information, LdapcpLoggingService.Categories.LDAPCP);

            if (ProcessedAttributes == null)
                return;

            this.Lock_Config.EnterReadLock();
            try
            {
                foreach (var attribute in ProcessedAttributes.Where(x => !String.IsNullOrEmpty(x.ClaimType)))
                {
                    claimTypes.Add(attribute.ClaimType);
                }
            }
            finally
            {
                this.Lock_Config.ExitReadLock();
            }
        }

        protected override void FillClaimValueTypes(List<string> claimValueTypes)
        {
            if (claimValueTypes == null)
                throw new ArgumentNullException("claimValueTypes");

            LDAPCP.LogToULS(String.Format("[{0}] FillClaimValueTypes called, ProcessedAttributes null: {1}", ProviderInternalName, ProcessedAttributes == null ? true : false),
                            TraceSeverity.VerboseEx, EventSeverity.Information, LdapcpLoggingService.Categories.LDAPCP);

            if (ProcessedAttributes == null)
                return;

            this.Lock_Config.EnterReadLock();
            try
            {
                foreach (var attribute in ProcessedAttributes.Where(x => !String.IsNullOrEmpty(x.ClaimValueType)))
                {
                    claimValueTypes.Add(attribute.ClaimValueType);
                }
            }
            finally
            {
                this.Lock_Config.ExitReadLock();
            }
        }

        protected override void FillClaimsForEntity(Uri context, SPClaim entity, List<SPClaim> claims)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// the type of SPClaimEntityTypes that this provider support, such as SPClaimEntityTypes.User or SPClaimEntityTypes.FormsRole
        /// </summary>
        /// <param name="entityTypes"></param>
        protected override void FillEntityTypes(List<string> entityTypes)
        {
            LDAPCP.LogToULS(String.Format("[{0}] FillEntityTypes called, ProcessedAttributes null: {1}", ProviderInternalName, ProcessedAttributes == null ? true : false),
                            TraceSeverity.VerboseEx, EventSeverity.Information, LdapcpLoggingService.Categories.LDAPCP);

            if (ProcessedAttributes == null)
                return;

            this.Lock_Config.EnterReadLock();
            try
            {
                var spUniqueEntitytypes = from attributes in ProcessedAttributes
                                          where attributes.ClaimEntityType != null
                                          group attributes by new { claimEntityType = attributes.ClaimEntityType } into groupedByEntityType
                                          select new { value = groupedByEntityType.Key.claimEntityType };

                if (null == spUniqueEntitytypes) return;

                foreach (var spEntityType in spUniqueEntitytypes)
                {
                    entityTypes.Add(spEntityType.value);
                }
            }
            finally
            {
                this.Lock_Config.ExitReadLock();
            }
        }

        /// <summary>
        /// Populates the list of attributes in the left side of the people picker
        /// </summary>
        /// <param name="context">the current web application</param>
        /// <param name="entityTypes">the type of SPClaimEntityTypes we should return</param>
        /// <param name="hierarchyNodeID"></param>
        /// <param name="numberOfLevels"></param>
        /// <param name="hierarchy"></param>
        protected override void FillHierarchy(Uri context, string[] entityTypes, string hierarchyNodeID, int numberOfLevels, Microsoft.SharePoint.WebControls.SPProviderHierarchyTree hierarchy)
        {
            LogToULS(String.Format("[{0}] FillHierarchy called", ProviderInternalName),
                TraceSeverity.VerboseEx, EventSeverity.Information, LdapcpLoggingService.Categories.LDAPCP);

            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                if (!Initialize(context, entityTypes))
                    return;

                this.Lock_Config.EnterReadLock();
                try
                {
                    if (hierarchyNodeID == null)
                    {
                        // Root level
                        foreach (var attribute in ProcessedAttributes.Where(x => !x.CreateAsIdentityClaim && entityTypes.Contains(x.ClaimEntityType)))
                        {
                            hierarchy.AddChild(
                                new Microsoft.SharePoint.WebControls.SPProviderHierarchyNode(
                                    _ProviderInternalName,
                                    attribute.ClaimTypeMappingName,
                                    attribute.ClaimType,
                                    true));
                        }
                    }
                }
                catch (Exception ex)
                {
                    LogToULS(String.Format("[{0}] Unexpected error in FillHierarchy: {1}. Callstack: {2}", ProviderInternalName, ex.Message, ex.StackTrace), TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.Claims_Picking);
                }
                finally
                {
                    this.Lock_Config.ExitReadLock();
                }
            });
        }

        /// <summary>
        /// PickerEntity is resolved (underlined) but claim must be resolved to provide again a PickerEntity so that SharePoint can actually create the permission
        /// </summary>
        /// <param name="context"></param>
        /// <param name="entityTypes"></param>
        /// <param name="resolveInput"></param>
        /// <param name="resolved"></param>
        protected override void FillResolve(Uri context, string[] entityTypes, SPClaim resolveInput, List<Microsoft.SharePoint.WebControls.PickerEntity> resolved)
        {
            LDAPCP.LogToULS(String.Format("[{0}] FillResolve(SPClaim) called, incoming claim value: \"{1}\", claim type: \"{2}\", claim issuer: \"{3}\"", ProviderInternalName, resolveInput.Value, resolveInput.ClaimType, resolveInput.OriginalIssuer),
                            TraceSeverity.VerboseEx, EventSeverity.Information, LdapcpLoggingService.Categories.LDAPCP);

            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                if (!Initialize(context, entityTypes))
                    return;

                // Ensure incoming claim should be validated by LDAPCP
                // Must be made after call to Initialize because SPTrustedLoginProvider name must be known
                if (!String.Equals(resolveInput.OriginalIssuer, IssuerName, StringComparison.InvariantCultureIgnoreCase))
                    return;

                this.Lock_Config.EnterReadLock();
                try
                {
                    // Resolve value only against the incoming claim type that uniquely identifies the user (mail, sAMAccountName)
                    var attributes = this.ProcessedAttributes.FindAll(x =>
                        String.Equals(x.ClaimType, resolveInput.ClaimType, StringComparison.InvariantCultureIgnoreCase)
                        && !x.CreateAsIdentityClaim);
                    if (attributes.Count != 1)
                    {
                        // Should always find only 1 attribute at this stage
                        LogToULS(String.Format("[{0}] Found {1} attributes that match the claim type \"{2}\", but exactly 1 is expected. Verify that there is no duplicate claim type. Aborting operation.", ProviderInternalName, attributes.Count().ToString(), resolveInput.ClaimType), TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.Claims_Picking);
                        return;
                    }
                    AttributeHelper attribute = attributes.First();
                    string input = (!String.IsNullOrEmpty(attribute.PrefixToAddToValueReturnedProp) && resolveInput.Value.StartsWith(attribute.PrefixToAddToValueReturnedProp, StringComparison.InvariantCultureIgnoreCase)) ?
                        resolveInput.Value.Substring(attribute.PrefixToAddToValueReturnedProp.Length) : resolveInput.Value;

                    if (this.CurrentConfiguration.AlwaysResolveUserInputProp)
                    {
                        // On purpose, if this property is true, LDAP lookp is completely bypassed
                        PickerEntity entity = CreatePickerEntityForSpecificClaimType(
                            input,
                            attribute,
                            false);
                        if (entity != null)
                        {
                            resolved.Add(entity);
                            LogToULS(String.Format("[{0}] Validated permission without LDAP lookup because LDAPCP configured to always resolve input. Claim value: \"{1}\", Claim type: \"{2}\"", ProviderInternalName, entity.Claim.Value, entity.Claim.ClaimType), TraceSeverity.Medium, EventSeverity.Information, LdapcpLoggingService.Categories.Claims_Picking);
                            return;
                        }
                    }
                    else
                    {
                        // At this stage, it is impossible to know if user typed input with a keyword so that it should be validated without AAD lookup
                        // So best we can do is to check whether this claim type can be resolved without lookup, and create permission if so
                        if (!String.IsNullOrEmpty(attribute.PrefixToBypassLookup))
                        {
                            bool inputHasKeyword = (!String.IsNullOrEmpty(attribute.PrefixToAddToValueReturnedProp) && !resolveInput.Value.StartsWith(attribute.PrefixToAddToValueReturnedProp, StringComparison.InvariantCultureIgnoreCase) && attribute.DoNotAddPrefixIfInputHasKeywordProp) ? true : false;
                            PickerEntity entity = CreatePickerEntityForSpecificClaimType(
                                input,
                                attribute,
                                inputHasKeyword);
                            if (entity != null)
                            {
                                resolved.Add(entity);
                                LogToULS(String.Format("[{0}] Validated permission without LDAP lookup because corresponding claim type has a keyword associated. Claim value: \"{1}\", Claim type: \"{2}\"", ProviderInternalName, entity.Claim.Value, entity.Claim.ClaimType), TraceSeverity.Medium, EventSeverity.Information, LdapcpLoggingService.Categories.Claims_Picking);
                                return;
                            }
                        }
                        else
                        {
                            // Claims provider is called by static methods in SPClaimProviderOperations class. As a consequence, results must be declared in the method (and not in the class) to ensure that each thread has it own unique collection
                            ConsolidatedResultCollection results = new ConsolidatedResultCollection();
                            BuildFilterAndProcessResults(input, attributes, true, context, entityTypes, ref results);
                            if (results != null && results.Count == 1)
                            {
                                resolved.Add(results[0].PickerEntity);
                                LogToULS(String.Format("[{0}] Validated permission with LDAP lookup. Claim value: \"{1}\", Claim type: \"{2}\"", ProviderInternalName, results[0].PickerEntity.Claim.Value, results[0].PickerEntity.Claim.ClaimType), TraceSeverity.Medium, EventSeverity.Information, LdapcpLoggingService.Categories.Claims_Picking);
                                return;
                            }
                            else if (results != null && results.Count != 1)
                            {
                                LogToULS(String.Format("[{0}] Validation with LDAP lookup created {1} permissions instead of 1 expected. Aborting operation", ProviderInternalName, results.Count.ToString()), TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.Claims_Picking);
                                return;
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    LogToULS(String.Format("[{0}] Unexpected error in FillResolve: {1}. Callstack: {2}", ProviderInternalName, ex.Message, ex.StackTrace), TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.Claims_Picking);
                }
                finally
                {
                    this.Lock_Config.ExitReadLock();
                }
            });
        }

        /// <summary>
        /// Called during a search in the small people picker control
        /// </summary>
        /// <param name="context"></param>
        /// <param name="entityTypes"></param>
        /// <param name="resolveInput"></param>
        /// <param name="resolved"></param>
        protected override void FillResolve(Uri context, string[] entityTypes, string resolveInput, List<Microsoft.SharePoint.WebControls.PickerEntity> resolved)
        {
            LDAPCP.LogToULS(String.Format("[{0}] FillResolve(string) called, incoming input \"{1}\"", ProviderInternalName, resolveInput),
                            TraceSeverity.VerboseEx, EventSeverity.Information, LdapcpLoggingService.Categories.LDAPCP);

            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                if (!Initialize(context, entityTypes))
                    return;

                this.Lock_Config.EnterReadLock();
                try
                {
                    string input = resolveInput;
                    // List<T>.FindAll returns an empty list if no result found: http://msdn.microsoft.com/en-us/library/fh1w7y8z(v=vs.110).aspx
                    List<AttributeHelper> attributes = ProcessedAttributes.FindAll(x => entityTypes.Contains(x.ClaimEntityType));

                    if (this.CurrentConfiguration.AlwaysResolveUserInputProp)
                    {
                        // On purpose, if this property is true, LDAP lookp is completely bypassed
                        List<PickerEntity> entities = CreatePickerEntityForSpecificClaimTypes(
                            input,
                            attributes.FindAll(x => !x.CreateAsIdentityClaim),
                            false);
                        if (entities != null)
                        {
                            foreach (var entity in entities)
                            {
                                resolved.Add(entity);
                                LogToULS(String.Format("[{0}] Added permission created without LDAP lookup because LDAPCP configured to always resolve input: claim value: {1}, claim type: \"{2}\"", ProviderInternalName, entity.Claim.Value, entity.Claim.ClaimType), TraceSeverity.Medium, EventSeverity.Information, LdapcpLoggingService.Categories.Claims_Picking);
                            }
                        }
                    }
                    else
                    {
                        // Check if input starts with an AzureADObject.PrefixToBypassLookup
                        List<AttributeHelper> attribsMatchInputPrefix = attributes.FindAll(x =>
                            !String.IsNullOrEmpty(x.PrefixToBypassLookup) &&
                            input.StartsWith(x.PrefixToBypassLookup, StringComparison.InvariantCultureIgnoreCase));
                        if (attribsMatchInputPrefix.Count > 0)
                        {
                            // Input has a prefix, so it should be validated with no lookup
                            AttributeHelper attribMatchInputPrefix = attribsMatchInputPrefix.First();
                            if (attribsMatchInputPrefix.Count > 1)
                            {
                                // Multiple attributes have same prefix, which is bad
                                LogToULS(String.Format("[{0}] Multiple attributes have same prefix {1}, which is bad.", ProviderInternalName, attribMatchInputPrefix.PrefixToBypassLookup), TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.Claims_Picking);
                                return;
                            }

                            // Get PickerEntity from the current objectMatchingInputPrefix
                            PickerEntity entity = CreatePickerEntityForSpecificClaimType(
                                input.Substring(attribMatchInputPrefix.PrefixToBypassLookup.Length),
                                attribMatchInputPrefix,
                                true);
                            if (entity != null)
                            {
                                resolved.Add(entity);
                                LogToULS(String.Format("[{0}] Added permission created without LDAP lookup because input matches a keyword: claim value: \"{1}\", claim type: \"{2}\"", ProviderInternalName, entity.Claim.Value, entity.Claim.ClaimType), TraceSeverity.Medium, EventSeverity.Information, LdapcpLoggingService.Categories.Claims_Picking);
                            }
                        }
                        else
                        {
                            // Perform LDAP lookup
                            // Claims provider is called by static methods in SPClaimProviderOperations class. As a consequence, results must be declared in the method (and not in the class) to ensure that each thread has it own unique collection
                            ConsolidatedResultCollection results = new ConsolidatedResultCollection();
                            BuildFilterAndProcessResults(
                                input,
                                attributes,
                                this.CurrentConfiguration.FilterExactMatchOnlyProp,
                                context,
                                entityTypes,
                                ref results);

                            if (results != null && results.Count > 0)
                            {
                                foreach (var result in results)
                                {
                                    resolved.Add(result.PickerEntity);
                                    LogToULS(String.Format("[{0}] Added permission created with LDAP lookup: claim value: \"{1}\", claim type: \"{2}\"", ProviderInternalName, result.PickerEntity.Claim.Value, result.PickerEntity.Claim.ClaimType), TraceSeverity.Medium, EventSeverity.Information, LdapcpLoggingService.Categories.Claims_Picking);
                                }
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    LogToULS(String.Format("[{0}] Unexpected error in FillResolve(string): {1}. Callstack: {2}", ProviderInternalName, ex.Message, ex.StackTrace), TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.Claims_Picking);
                }
                finally
                {
                    this.Lock_Config.ExitReadLock();
                }
            });
        }

        protected virtual string AddAttributeToFilter(AttributeHelper attribute, string searchPattern)
        {
            string filter = String.Empty;
            string additionalFilter = String.Empty;

            if (this.CurrentConfiguration.FilterSecurityGroupsOnlyProp && String.Equals(attribute.LDAPObjectClassProp, "group", StringComparison.OrdinalIgnoreCase))
                additionalFilter = FilterSecurityGroupsOnlyLDAPFilter;

            if (!String.IsNullOrEmpty(attribute.AdditionalLDAPFilterProp))
                additionalFilter += attribute.AdditionalLDAPFilterProp;

            filter = String.Format(LDAPFilter, attribute.LDAPAttribute, searchPattern, attribute.LDAPObjectClassProp, additionalFilter);
            return filter;
        }



        /// <summary>
        /// Create the SPClaim with proper trust name
        /// </summary>
        /// <param name="type">Claim type</param>
        /// <param name="value">Claim value</param>
        /// <param name="valueType">Claim valueType</param>
        /// <param name="inputHasKeyword">Did the original input contain a keyword?</param>
        /// <returns></returns>
        protected virtual SPClaim CreateClaim(string type, string value, string valueType, bool inputHasKeyword)
        {
            string claimValue = String.Empty;
            //var attr = ProcessedAttributes.Where(x => x.ClaimTypeProp == type).FirstOrDefault();
            var attr = ProcessedAttributes.FirstOrDefault(x => String.Equals(x.ClaimType, type, StringComparison.InvariantCultureIgnoreCase));
            //if (inputHasKeyword && attr.DoNotAddPrefixIfInputHasKeywordProp)
            if (!inputHasKeyword || !attr.DoNotAddPrefixIfInputHasKeywordProp) claimValue = attr.PrefixToAddToValueReturnedProp;
            claimValue += value;
            // SPClaimProvider.CreateClaim issues with SPOriginalIssuerType.ClaimProvider
            //return CreateClaim(type, claimValue, valueType);
            return new SPClaim(type, claimValue, valueType, IssuerName);
        }

        protected virtual PickerEntity CreatePickerEntityHelper(ConsolidatedResult result)
        {
            PickerEntity pe = CreatePickerEntity();
            SPClaim claim;
            string permissionDisplayText = String.Empty;
            string permissionValue = result.Value;
            string permissionClaimType = result.Attribute.ClaimType;
            bool isIdentityClaimType = false;

            if (String.Equals(permissionClaimType, SPTrust.IdentityClaimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase)
                || result.Attribute.CreateAsIdentityClaim)
            {
                isIdentityClaimType = true;
            }

            if (result.Attribute.CreateAsIdentityClaim)
            {
                // This attribute is not directly linked to a claim type, so permission is created with identity claim type
                permissionClaimType = IdentityAttribute.ClaimType;
                permissionValue = FormatPermissionValue(permissionClaimType, result.LDAPResults[IdentityAttribute.LDAPAttribute][0].ToString(), result.NetBiosName, isIdentityClaimType, result);
                claim = CreateClaim(
                    permissionClaimType,
                    permissionValue,
                    IdentityAttribute.ClaimValueType,
                    false);
                pe.EntityType = IdentityAttribute.ClaimEntityType;
            }
            else
            {
                permissionValue = FormatPermissionValue(result.Attribute.ClaimType, permissionValue, result.NetBiosName, isIdentityClaimType, result);
                claim = CreateClaim(
                    permissionClaimType,
                    permissionValue,
                    result.Attribute.ClaimValueType,
                    false);
                pe.EntityType = result.Attribute.ClaimEntityType;
            }

            permissionDisplayText = FormatPermissionDisplayText(permissionClaimType, permissionValue, isIdentityClaimType, result);
            pe.DisplayText = permissionDisplayText;
            pe.Description = String.Format(
                    PickerEntityOnMouseOver,
                    result.Attribute.LDAPAttribute,
                    permissionValue);
            pe.Claim = claim;
            pe.IsResolved = true;
            pe.EntityGroupName = this.CurrentConfiguration.PickerEntityGroupNameProp;

            int nbMetadata = 0;
            if (result.Attribute.ClaimEntityType == SPClaimEntityTypes.User)
            {
                // Populate metadata attributes of permission created
                foreach (var entityAttrib in UserMetadataAttributes)
                {
                    // if there is actally a value in the LDAP result, then it can be set
                    if (result.LDAPResults.Contains(entityAttrib.LDAPAttribute) && result.LDAPResults[entityAttrib.LDAPAttribute].Count > 0)
                    {
                        pe.EntityData[entityAttrib.EntityDataKey] = result.LDAPResults[entityAttrib.LDAPAttribute][0].ToString();
                        nbMetadata++;
                        LogToULS(String.Format("[{0}] Added metadata \"{1}\" with value \"{2}\" to permission", ProviderInternalName, entityAttrib.EntityDataKey, pe.EntityData[entityAttrib.EntityDataKey]), TraceSeverity.Verbose, EventSeverity.Information, LdapcpLoggingService.Categories.Claims_Picking);
                    }
                }
            }

            LogToULS(String.Format("[{0}] Created permission: display text: \"{1}\", value: \"{2}\", claim type: \"{3}\", and filled with {4} metadata.", ProviderInternalName, pe.DisplayText, pe.Claim.Value, pe.Claim.ClaimType, nbMetadata.ToString()), TraceSeverity.Verbose, EventSeverity.Information, LdapcpLoggingService.Categories.Claims_Picking);
            return pe;
        }

        /// <summary>
        /// Override this method to customize value of permission created
        /// </summary>
        /// <param name="claimType"></param>
        /// <param name="claimValue"></param>
        /// <param name="netBiosName"></param>
        /// <returns></returns>
        protected virtual string FormatPermissionValue(string claimType, string claimValue, string netBiosName, bool isIdentityClaimType, ConsolidatedResult result)
        {
            return claimValue;
        }

        /// <summary>
        /// Override this method to customize display text of permission created
        /// </summary>
        /// <param name="displayText"></param>
        /// <param name="claimType"></param>
        /// <param name="claimValue"></param>
        /// <param name="isIdentityClaim"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        protected virtual string FormatPermissionDisplayText(string claimType, string claimValue, bool isIdentityClaimType, ConsolidatedResult result)
        {
            string permissionDisplayText = String.Empty;
            string valueDisplayedInPermission = String.Empty;
            bool displayLdapMatchForIdentityClaimType = false;

            //if (result == null)
            //{
            //    // Permission created without actual lookup
            //    if (isIdentityClaimType) return claimValue;
            //    else return String.Format(PickerEntityDisplayText, claimTypeToResolve.ClaimTypeMappingName, claimValue);
            //}

            if (isIdentityClaimType) displayLdapMatchForIdentityClaimType = this.CurrentConfiguration.DisplayLdapMatchForIdentityClaimTypeProp;

            if (!String.IsNullOrEmpty(result.Attribute.LDAPAttributeToDisplayProp) && result.LDAPResults.Contains(result.Attribute.LDAPAttributeToDisplayProp))
            {   // AttributeHelper is set to use a specific LDAP attribute as display text of permission
                if (!isIdentityClaimType)
                    permissionDisplayText = "(" + result.Attribute.ClaimTypeMappingName + ") ";
                permissionDisplayText += valueDisplayedInPermission = result.LDAPResults[result.Attribute.LDAPAttributeToDisplayProp][0].ToString();
            }
            else
            {   // AttributeHelper is set to use its actual LDAP attribute as display text of permission
                if (!isIdentityClaimType)
                {
                    valueDisplayedInPermission = claimValue;
                    permissionDisplayText = String.Format(
                        PickerEntityDisplayText,
                        result.Attribute.ClaimTypeMappingName,
                        valueDisplayedInPermission);
                }
                else
                {   // Always specifically use LDAP attribute of identity claim type
                    permissionDisplayText = valueDisplayedInPermission = result.LDAPResults[IdentityAttribute.LDAPAttribute][0].ToString();
                }
            }

            // Check if LDAP value that actually resolved this result should be included in the display text of the permission
            if (displayLdapMatchForIdentityClaimType && result.LDAPResults.Contains(result.Attribute.LDAPAttribute)
                && !String.Equals(valueDisplayedInPermission, claimValue, StringComparison.InvariantCultureIgnoreCase))
            {
                permissionDisplayText += String.Format(" ({0})", claimValue);
            }

            return permissionDisplayText;
        }

        /// <summary>
        /// Create a PickerEntity of the input for the claim type specified in parameter
        /// </summary>
        /// <param name="input">Value of the permission</param>
        /// <param name="claimTypesToResolve">claim type of the permission</param>
        /// <param name="inputHasKeyword">Did the original input contain a keyword?</param>
        /// <returns></returns>
        protected virtual PickerEntity CreatePickerEntityForSpecificClaimType(string input, AttributeHelper claimTypesToResolve, bool inputHasKeyword)
        {
            List<PickerEntity> entities = CreatePickerEntityForSpecificClaimTypes(
                input,
                new List<AttributeHelper>()
                    {
                        claimTypesToResolve,
                    },
                inputHasKeyword);
            return entities == null ? null : entities.First();
        }

        /// <summary>
        /// Create a PickerEntity of the input for each claim type specified in parameter
        /// </summary>
        /// <param name="input">Value of the permission</param>
        /// <param name="claimTypesToResolve">claim types of the permission</param>
        /// <param name="inputHasKeyword">Did the original input contain a keyword?</param>
        /// <returns></returns>
        protected virtual List<PickerEntity> CreatePickerEntityForSpecificClaimTypes(string input, List<AttributeHelper> claimTypesToResolve, bool inputHasKeyword)
        {
            List<PickerEntity> entities = new List<PickerEntity>();
            foreach (var claimTypeToResolve in claimTypesToResolve)
            {
                PickerEntity pe = CreatePickerEntity();
                SPClaim claim = CreateClaim(claimTypeToResolve.ClaimType, input, claimTypeToResolve.ClaimValueType, inputHasKeyword);

                if (String.Equals(claim.ClaimType, SPTrust.IdentityClaimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase))
                {
                    pe.DisplayText = input;
                }
                else
                {
                    pe.DisplayText = String.Format(
                        PickerEntityDisplayText,
                        claimTypeToResolve.ClaimTypeMappingName,
                        input);
                }

                pe.EntityType = claimTypeToResolve.ClaimEntityType;
                pe.Description = String.Format(
                    PickerEntityOnMouseOver,
                    claimTypeToResolve.LDAPAttribute,
                    input);

                pe.Claim = claim;
                pe.IsResolved = true;
                pe.EntityGroupName = this.CurrentConfiguration.PickerEntityGroupNameProp;

                if (claimTypeToResolve.ClaimEntityType == SPClaimEntityTypes.User && !String.IsNullOrEmpty(claimTypeToResolve.EntityDataKey))
                {
                    pe.EntityData[claimTypeToResolve.EntityDataKey] = pe.Claim.Value;
                    LogToULS(String.Format("[{0}] Added metadata \"{1}\" with value \"{2}\" to permission", ProviderInternalName, claimTypeToResolve.EntityDataKey, pe.EntityData[claimTypeToResolve.EntityDataKey]), TraceSeverity.Verbose, EventSeverity.Information, LdapcpLoggingService.Categories.Claims_Picking);
                }
                entities.Add(pe);
                LogToULS(String.Format("[{0}] Created permission: display text: \"{1}\", value: \"{2}\", claim type: \"{3}\".", ProviderInternalName, pe.DisplayText, pe.Claim.Value, pe.Claim.ClaimType), TraceSeverity.Verbose, EventSeverity.Information, LdapcpLoggingService.Categories.Claims_Picking);
            }
            return entities.Count > 0 ? entities : null;
        }

        protected override void FillSchema(Microsoft.SharePoint.WebControls.SPProviderSchema schema)
        {
        }

        protected override void FillSearch(Uri context, string[] entityTypes, string searchPattern, string hierarchyNodeID, int maxCount, Microsoft.SharePoint.WebControls.SPProviderHierarchyTree searchTree)
        {
            LDAPCP.LogToULS(String.Format("[{0}] FillSearch called, incoming input: \"{1}\"", ProviderInternalName, searchPattern),
                TraceSeverity.VerboseEx, EventSeverity.Information, LdapcpLoggingService.Categories.LDAPCP);

            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                if (!Initialize(context, entityTypes))
                    return;

                this.Lock_Config.EnterReadLock();
                try
                {
                    string input = searchPattern;
                    SPProviderHierarchyNode matchNode = null;
                    List<AttributeHelper> attributes;
                    if (!String.IsNullOrEmpty(hierarchyNodeID))
                    {
                        // Restrict search to attributes currently selected in the hierarchy (may return multiple results if identity claim type)
                        attributes = ProcessedAttributes.FindAll(x =>
                            String.Equals(x.ClaimType, hierarchyNodeID, StringComparison.InvariantCultureIgnoreCase) &&
                            entityTypes.Contains(x.ClaimEntityType));
                    }
                    else
                    {
                        attributes = ProcessedAttributes.FindAll(x => entityTypes.Contains(x.ClaimEntityType));
                    }

                    if (this.CurrentConfiguration.AlwaysResolveUserInputProp)
                    {
                        // On purpose, if this property is true, LDAP lookp is completely bypassed
                        List<PickerEntity> entities = CreatePickerEntityForSpecificClaimTypes(
                            input,
                            attributes.FindAll(x => !x.CreateAsIdentityClaim),
                            false);
                        if (entities != null)
                        {
                            foreach (var entity in entities)
                            {
                                // Add current PickerEntity to the corresponding attribute in the hierarchy
                                // Use Claim type has key
                                string entityClaimType = entity.Claim.ClaimType;
                                // ClaimTypeMappingName cannot be null as it is value of SPClaimTypeMapping.IncomingClaimTypeDisplayName, which is mandatory
                                string ClaimTypeMappingName = attributes
                                    .First(x =>
                                        !x.CreateAsIdentityClaim &&
                                        String.Equals(x.ClaimType, entityClaimType, StringComparison.InvariantCultureIgnoreCase))
                                    .ClaimTypeMappingName;

                                if (searchTree.HasChild(entityClaimType))
                                {
                                    matchNode = searchTree.Children.First(x => String.Equals(x.HierarchyNodeID, entityClaimType, StringComparison.InvariantCultureIgnoreCase));
                                }
                                else
                                {
                                    matchNode = new SPProviderHierarchyNode(_ProviderInternalName, ClaimTypeMappingName, entityClaimType, true);
                                    searchTree.AddChild(matchNode);
                                }
                                matchNode.AddEntity(entity);
                                LogToULS(String.Format("[{0}] Added permission created without LDAP lookup because LDAPCP configured to always resolve input: claim value: \"{1}\", claim type: \"{2}\" to the list of results.", ProviderInternalName, entity.Claim.Value, entity.Claim.ClaimType), TraceSeverity.Medium, EventSeverity.Information, LdapcpLoggingService.Categories.Claims_Picking);
                            }
                        }
                    }
                    else
                    {
                        // Check if input starts with PrefixToBypassLookup in a AttributeHelper
                        List<AttributeHelper> attribsMatchingInputPrefix = attributes.FindAll(x =>
                            !String.IsNullOrEmpty(x.PrefixToBypassLookup) &&
                            input.StartsWith(x.PrefixToBypassLookup, StringComparison.InvariantCultureIgnoreCase));
                        if (attribsMatchingInputPrefix.Count > 0)
                        {
                            // Input has a prefix, so it should be validated with no lookup
                            AttributeHelper attribMatchingInputPrefix = attribsMatchingInputPrefix.First();
                            if (attribsMatchingInputPrefix.Count > 1)
                            {
                                // Multiple attributes have same prefix, which is bad
                                LogToULS(String.Format("[{0}] Multiple attributes have same prefix {1}, which is bad.", ProviderInternalName, attribMatchingInputPrefix.PrefixToBypassLookup), TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.Claims_Picking);
                                return;
                            }

                            PickerEntity entity = CreatePickerEntityForSpecificClaimType(
                                input.Substring(attribMatchingInputPrefix.PrefixToBypassLookup.Length),
                                attribMatchingInputPrefix,
                                true);

                            if (searchTree.HasChild(attribMatchingInputPrefix.ClaimType))
                            {
                                matchNode = searchTree.Children.First(x => String.Equals(x.HierarchyNodeID, attribMatchingInputPrefix.ClaimType, StringComparison.InvariantCultureIgnoreCase));
                            }
                            else
                            {
                                matchNode = new SPProviderHierarchyNode(_ProviderInternalName, attribMatchingInputPrefix.ClaimTypeMappingName, attribMatchingInputPrefix.ClaimType, true);
                                searchTree.AddChild(matchNode);
                            }
                            matchNode.AddEntity(entity);
                            LogToULS(String.Format("[{0}] Added permission created without LDAP lookup because input matches a keyword: claim value: \"{1}\", claim type: \"{2}\" to the list of results.", ProviderInternalName, entity.Claim.Value, entity.Claim.ClaimType), TraceSeverity.Medium, EventSeverity.Information, LdapcpLoggingService.Categories.Claims_Picking);
                        }
                        else
                        {
                            // Claims provider is called by static methods in SPClaimProviderOperations class. As a consequence, consolidatedResults must be declared in the method (and not in the class) to ensure that each thread has it own unique collection
                            ConsolidatedResultCollection results = new ConsolidatedResultCollection();
                            BuildFilterAndProcessResults(
                                input,
                                attributes,
                                this.CurrentConfiguration.FilterExactMatchOnlyProp,
                                context,
                                entityTypes,
                                ref results);

                            if (results != null && results.Count > 0)
                            {
                                foreach (var result in results)
                                {
                                    // Add current PickerEntity to the corresponding attribute in the hierarchy
                                    if (searchTree.HasChild(result.Attribute.ClaimType))
                                    {
                                        matchNode = searchTree.Children.First(x => x.HierarchyNodeID == result.Attribute.ClaimType);
                                    }
                                    else
                                    {
                                        matchNode = new SPProviderHierarchyNode(_ProviderInternalName, result.Attribute.ClaimTypeMappingName, result.Attribute.ClaimType, true);
                                        searchTree.AddChild(matchNode);
                                    }
                                    matchNode.AddEntity(result.PickerEntity);
                                    LogToULS(String.Format("[{0}] Added permission created with LDAP lookup: claim value: \"{1}\", claim type: \"{2}\" to the list of results.", ProviderInternalName, result.PickerEntity.Claim.Value, result.PickerEntity.Claim.ClaimType), TraceSeverity.Medium, EventSeverity.Information, LdapcpLoggingService.Categories.Claims_Picking);
                                }
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    LogToULS(String.Format("[{0}] Unexpected error in FillSearch: {1}. Callstack: {2}", ProviderInternalName, ex.Message, ex.StackTrace), TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.Claims_Picking);
                }
                finally
                {
                    this.Lock_Config.ExitReadLock();
                }
            });
        }

        public override string Name { get { return ProviderInternalName; } }
        public override bool SupportsEntityInformation { get { return false; } }
        public override bool SupportsHierarchy { get { return true; } }
        public override bool SupportsResolve { get { return true; } }
        public override bool SupportsSearch { get { return true; } }

        /// <summary>
        /// Override this method to set LDAP connections
        /// </summary>
        /// <param name="context">The context, as a URI</param>
        /// <param name="entityTypes">The EntityType entity types set to scope the search to</param>
        /// <returns></returns>
        protected virtual DirectoryEntry[] SetLDAPConnections(Uri context, string[] entityTypes)
        {
            if (this.CurrentConfiguration.LDAPConnectionsProp == null) return null;
            DirectoryEntry[] directoryEntries = new DirectoryEntry[this.CurrentConfiguration.LDAPConnectionsProp.Count()];
            int i = 0;
            foreach (var LDAPConnection in this.CurrentConfiguration.LDAPConnectionsProp)
            {
                if (!LDAPConnection.UserServerDirectoryEntry)
                {
                    LogToULS(String.Format("[{0}] Connect as {1} to {2}.", ProviderInternalName, LDAPConnection.Username, LDAPConnection.Path), TraceSeverity.Medium, EventSeverity.Information, LdapcpLoggingService.Categories.LDAP_Lookup);
                    directoryEntries[i++] = new DirectoryEntry(LDAPConnection.Path, LDAPConnection.Username, LDAPConnection.Password, LDAPConnection.AuthenticationTypes);
                }
                else
                {
                    LogToULS(String.Format("[{0}] Connect to AD this server is member of, with application pool credentials", ProviderInternalName), TraceSeverity.Medium, EventSeverity.Information, LdapcpLoggingService.Categories.LDAP_Lookup);
                    directoryEntries[i++] = Domain.GetComputerDomain().GetDirectoryEntry();
                }
            }
            return directoryEntries;
        }

        /// <summary>
        /// Query LDAP to find matches
        /// </summary>
        /// <param name="filter">looks like "(&(objectClass=user) ( |(cn=value) (mail=value)))"</param>
        /// <returns>true if 1 or more objects are found</returns>
        //protected bool SearchObjectsInLDAP(string filter, Uri context, string[] entityTypes, ref SearchResult[] LDAPSearchResults)
        protected bool SearchObjectsInLDAP(string filter, Uri context, string[] entityTypes, ref List<LDAPSearchResultWrapper> LDAPSearchResults)
        {
            DirectoryEntry[] directories = SetLDAPConnections(context, entityTypes);
            if (directories == null)
            {
                LogToULS(String.Format("[{0}] No LDAP connection found", ProviderInternalName), TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.LDAP_Lookup);
                return false;
            }

            DirectorySearcher ds = new DirectorySearcher();
            ds.Filter = filter;
            ds.PropertiesToLoad.Add(LDAPObjectClassName);
            ds.PropertiesToLoad.Add("nETBIOSName");
            foreach (var ldapAttribute in ProcessedAttributes.Where(x => !String.IsNullOrEmpty(x.LDAPAttribute)))
            {
                ds.PropertiesToLoad.Add(ldapAttribute.LDAPAttribute);
                if (!String.IsNullOrEmpty(ldapAttribute.LDAPAttributeToDisplayProp)) ds.PropertiesToLoad.Add(ldapAttribute.LDAPAttributeToDisplayProp);
            }

            // Populate additional attributes that are not part of the filter but are requested in the result
            foreach (var metadataAttribute in UserMetadataAttributes)
            {
                if (!ds.PropertiesToLoad.Contains(metadataAttribute.LDAPAttribute)) ds.PropertiesToLoad.Add(metadataAttribute.LDAPAttribute);
            }

            string[] dsproperties = new string[ds.PropertiesToLoad.Count];
            ds.PropertiesToLoad.CopyTo(dsproperties, 0);

            // Query LDAP servers
            foreach (DirectoryEntry directory in directories)
            {
                using (new SPMonitoredScope(String.Format("[{0}] Connecting to LDAP server {1}", ProviderInternalName, directory.Path), 1000))
                {
                    try
                    {
                        DirectorySearcher currentDirectorySearcher = new DirectorySearcher(directory, ds.Filter, dsproperties);
                        using (SearchResultCollection directoryResults = currentDirectorySearcher.FindAll())
                        {
                            if (directoryResults != null && directoryResults.Count > 0)
                            {
                                foreach (SearchResult item in directoryResults)
                                {
                                    LDAPSearchResults.Add(new LDAPSearchResultWrapper()
                                    {
                                        SearchResult = item,
                                        LDAPNetBiosName = directory.Properties["name"].Value.ToString()
                                    });
                                }
                                LogToULS(String.Format("[{0}] Got {1} result(s) from {2}", ProviderInternalName, directoryResults.Count.ToString(), directory.Path), TraceSeverity.Verbose, EventSeverity.Information, LdapcpLoggingService.Categories.LDAP_Lookup);
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        LogToULS(String.Format("[{0}] Unexpected error during connection to LDAP server \"{1}\": {2}. Callstack: {3}", ProviderInternalName, directory.Path, ex.Message, ex.StackTrace), TraceSeverity.Unexpected, EventSeverity.Error, LdapcpLoggingService.Categories.LDAP_Lookup);
                    }
                    finally
                    {
                        directory.Dispose();
                    }
                }
            }
            return LDAPSearchResults.Count > 0;
        }

        public static void LogToULS(string message, TraceSeverity traceSeverity, EventSeverity eventSeverity, LdapcpLoggingService.Categories category)
        {
            try
            {
                LdapcpLoggingService.WriteTrace(category, traceSeverity, message);
                //LdapcpLoggingService.WriteEvent(LdapcpLoggingService.Categories.LDAPCP, eventSeverity, message);
            }
            catch
            {   // Don't want to do anything if logging goes wrong, just ignore and continue
            }
        }

        protected override void FillDefaultLocalizedDisplayName(System.Globalization.CultureInfo culture, out string localizedName)
        {
            if (SPTrust != null)
                localizedName = SPTrust.DisplayName;
            else
                base.FillDefaultLocalizedDisplayName(culture, out localizedName);
        }
    }

    public class ConsolidatedResult
    {
        public AttributeHelper Attribute;
        public PickerEntity PickerEntity;
        public ResultPropertyCollection LDAPResults;
        public int nbMatch = 0;
        public string Value;
        public string NetBiosName;
        //public string DEBUG;
    }

    public class LDAPSearchResultWrapper
    {
        public SearchResult SearchResult;
        public string LDAPNetBiosName;
    }

    /// <summary>
    /// Check if 2 attributes are identical (sams class and same name) to not add duplicates to the attributes list
    /// http://msdn.microsoft.com/en-us/library/bb338049
    /// </summary>
    class LDAPPropertiesComparer : IEqualityComparer<AttributeHelper>
    {
        // LDAP Attributes are equal if they have same LDAPAttribute and same LDAPObjectClassProp
        public bool Equals(AttributeHelper x, AttributeHelper y)
        {
            // Check whether the compared objects reference the same data.
            if (Object.ReferenceEquals(x, y)) return true;

            //Check whether any of the compared objects is null.
            if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
                return false;

            // Check if they have same LDAPAttribute and same LDAPObjectClassProp
            //return ((String.Compare(x.LDAPAttribute, y.LDAPAttribute, true) == 0) && (String.Compare(x.LDAPObjectClassProp, y.LDAPObjectClassProp, true) == 0));
            return ((String.Equals(x.LDAPAttribute, y.LDAPAttribute, StringComparison.OrdinalIgnoreCase)) && (String.Equals(x.LDAPObjectClassProp, y.LDAPObjectClassProp, StringComparison.OrdinalIgnoreCase)));
        }

        // If Equals() returns true for a pair of objects 
        // then GetHashCode() must return the same value for these objects.
        public int GetHashCode(AttributeHelper attribute)
        {
            //Check whether the object is null
            if (Object.ReferenceEquals(attribute, null)) return 0;

            // Add an extra space so that string can never be null and GetHashCode will never fail
            return (attribute.LDAPAttribute + " " + attribute.LDAPObjectClassProp).GetHashCode();
        }
    }

    public class ConsolidatedResultCollection : Collection<ConsolidatedResult>
    {
        /// <summary>
        /// Compare 2 results to not add duplicates
        /// they are identical if they have the same claim type and same value
        /// </summary>
        /// <param name="result">LDAP result to compare</param>
        /// <param name="attribute">AttributeHelper that matches result</param>
        /// <param name="compareWithLDAPNetBiosName">False by default. Set to true to consider that 2 identical results that don't come from same LDAP are NOT identical. Make sense only if values are modified later.</param>
        /// <returns></returns>
        public bool Contains(LDAPSearchResultWrapper result, AttributeHelper attribute, bool compareWithLDAPNetBiosName)
        {
            foreach (var item in base.Items)
            {
                if (item.Attribute.ClaimType != attribute.ClaimType)
                    continue;

                if (!item.LDAPResults.Contains(attribute.LDAPAttribute))
                    continue;

                // if compareWithLDAPNetBiosName is true, items are not considered as identical if they don't have same LDAPNetBiosName
                // compareWithLDAPNetBiosName shoud not be true unless some very specific scenario, like adding actual domain name afterwards to the value
                if (compareWithLDAPNetBiosName && !String.Equals(item.NetBiosName, result.LDAPNetBiosName, StringComparison.InvariantCultureIgnoreCase))
                    continue;

                if (String.Equals(item.LDAPResults[attribute.LDAPAttribute][0].ToString(), result.SearchResult.Properties[attribute.LDAPAttribute][0].ToString(), StringComparison.InvariantCultureIgnoreCase))
                {
                    item.nbMatch++;
                    return true;
                }
            }
            return false;
        }
    }
}
